namespace SimpleWebServices.Security
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Security.Cryptography;
    using System.Text;
    using System.Xml;

    public sealed class Digest
    {
        private static readonly CultureInfo _british = new CultureInfo("en-gb");

        public static string CurrentNonce
        {
            get
            {
                return FormatNonce(DateTimeFactory.UtcNow.AddMinutes(1));
            }
        }

        public string Algorithm
        {
            get;
            set;
        }

        public string CheckNonce
        {
            get;
            set;
        }

        public string Nonce
        {
            get;
            set;
        }

        public string NonceCount
        {
            get;
            set;
        }

        public string Opaque
        {
            get;
            set;
        }

        public string QualityOfProtection
        {
            get;
            set;
        }

        public string Realm
        {
            get;
            set;
        }

        public string Response
        {
            get;
            set;
        }

        public bool StaleNonce
        {
            get
            {
                return Digest.IsStaleNonce(this.Nonce);
            }
        }

        public RelativeUri Uri
        {
            get;
            set;
        }

        public string UserName
        {
            get;
            set;
        }

        public static string FormatAuthenticate(string realm, object stale)
        {
            if ((stale != null) && (stale.GetType() != typeof(bool)))
            {
                throw new ArgumentException("The value must be of type System.Boolean", "stale");
            }

            return FormatAuthenticate(realm, (null == stale) ? false : (bool)stale);
        }

        public static string FormatAuthenticate(string realm, bool stale)
        {
            return string.Format(CultureInfo.InvariantCulture, "Digest realm=\"{0}\", nonce=\"{1}\", opaque=\"0000000000000000\", stale={2}, algorithm=MD5, qop=\"auth\"", realm, CurrentNonce, XmlConvert.ToString((bool)stale));
        }

        public static string FormatNonce(DateTime value)
        {
            string date = value.ToString("G", _british);

            string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(date));

            return nonce.TrimEnd(new char[] { '=' }); /// nonce can't end in '=', so trim them from the end
        }

        public static string FormatResponse(string method, RelativeUri uri, string userName, string password, string realm, string nonce, string nc, string cnonce, string qop)
        {
            if (null == method)
            {
                throw new ArgumentNullException("method");
            }
            else if (0 == method.Length)
            {
                throw new ArgumentOutOfRangeException("method");
            }
            else if (null == uri)
            {
                throw new ArgumentNullException("uri");
            }
            else if (0 == uri.Length)
            {
                throw new ArgumentOutOfRangeException("uri");
            }
            else if (null == userName)
            {
                throw new ArgumentNullException("userName");
            }
            else if (0 == userName.Length)
            {
                throw new ArgumentOutOfRangeException("userName");
            }
            else if (null == password)
            {
                throw new ArgumentNullException("password");
            }
            else if (0 == password.Length)
            {
                throw new ArgumentOutOfRangeException("password");
            }
            else if (null == realm)
            {
                throw new ArgumentNullException("realm");
            }
            else if (0 == realm.Length)
            {
                throw new ArgumentOutOfRangeException("realm");
            }
            else if (null == nonce)
            {
                throw new ArgumentNullException("nonce");
            }
            else if (0 == nonce.Length)
            {
                throw new ArgumentOutOfRangeException("nonce");
            }

            string credentials = GetMD5HashBinHex(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", userName, realm, password));
            string request = GetMD5HashBinHex(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", method, uri));
            string digest = null;

            if (qop != null)
            {
                if (null == nc)
                {
                    throw new ArgumentNullException("nc");
                }
                else if (0 == nc.Length)
                {
                    throw new ArgumentOutOfRangeException("nc");
                }
                else if (null == cnonce)
                {
                    throw new ArgumentNullException("cnonce");
                }
                else if (0 == cnonce.Length)
                {
                    throw new ArgumentOutOfRangeException("cnonce");
                }
                else if (0 == qop.Length)
                {
                    throw new ArgumentOutOfRangeException("qop");
                }

                digest = string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}:{3}:{4}:{5}", credentials, nonce, nc, cnonce, qop, request);
            }
            else
            {
                digest = string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", credentials, nonce, request);
            }

            return GetMD5HashBinHex(digest);
        }

        public static IDictionary<string, string> GetElements(string authorization)
        {
            if (null == authorization)
            {
                throw new ArgumentNullException("authorization");
            }
            else if (0 == authorization.Length)
            {
                throw new ArgumentOutOfRangeException("authorization");
            }

            authorization = authorization.Trim();

            if ((authorization.Length < 6) || (authorization.IndexOf("Digest", 0, StringComparison.Ordinal) != 0))
            {
                throw new FormatException("The authorization must be prefixed with \"Digest\".");
            }

            IDictionary<string, string> elements = new Dictionary<string, string>();

            if ((authorization.Length != 6) && (authorization.Length != 7))
            {
                authorization = authorization.Substring(7);

                if (authorization.Length != 0)
                {
                    string[] split = authorization.Split(new char[] { ',' });

                    for (int i = 0; i < split.Length; i++)
                    {
                        string[] parts = split[i].Split(new char[] { '=' }, 2);
                        string key = parts[0].Trim(new char[] { ' ', '\"' });
                        string value = parts[1].Trim(new char[] { ' ', '\"' });

                        elements.Add(key, value);
                    }
                }
            }

            return elements;
        }

        public static string GetMD5HashBinHex(string value)
        {
            if (null == value)
            {
                throw new ArgumentNullException("value");
            }

            StringBuilder buffer = new StringBuilder();

            using (MD5 crypto = new MD5CryptoServiceProvider())
            {
                byte[] hash = crypto.ComputeHash(Encoding.ASCII.GetBytes(value));

                for (int i = 0; i < 16; i++)
                {
                    buffer.Append(string.Format(CultureInfo.InvariantCulture, "{0:x02}", hash[i]));
                }
            }

            return buffer.ToString();
        }

        public static bool IsStaleNonce(string nonce)
        {
            bool stale = true;

            if (!string.IsNullOrEmpty(nonce))
            {
                int i = nonce.Length % 4;

                if (i > 0)
                {
                    i = 4 - i;
                }

                string auth = nonce.PadRight(nonce.Length + i, '=');

                try
                {
                    stale = DateTimeFactory.UtcNow > DateTime.Parse(Encoding.ASCII.GetString(Convert.FromBase64String(auth)), _british);
                }
                catch (FormatException)
                {
                }
            }

            return stale;
        }

        public static Digest Parse(string authorization)
        {
            return Digest.Parse(Digest.GetElements(authorization));
        }

        public static Digest Parse(IDictionary<string, string> elements)
        {
            if (null == elements)
            {
                throw new ArgumentNullException("elements");
            }
            else if (0 == elements.Count)
            {
                throw new ArgumentOutOfRangeException("elements");
            }

            return new Digest
            {
                UserName = elements.ContainsKey("username") ? elements["username"] : null as string,
                Realm = elements.ContainsKey("realm") ? elements["realm"] : null as string,
                Nonce = elements.ContainsKey("nonce") ? elements["nonce"] : null as string,
                Uri = elements.ContainsKey("uri") ? elements["uri"] : null as string,
                CheckNonce = elements.ContainsKey("cnonce") ? elements["cnonce"] : null as string,
                NonceCount = elements.ContainsKey("nc") ? elements["nc"] : null as string,
                Algorithm = elements.ContainsKey("algorithm") ? elements["algorithm"] : null as string,
                Response = elements.ContainsKey("response") ? elements["response"] : null as string,
                QualityOfProtection = elements.ContainsKey("qop") ? elements["qop"] : null as string,
                Opaque = elements.ContainsKey("opaque") ? elements["opaque"] : null as string
            };
        }

        public bool Validate(string method, string realm, string password)
        {
            return !this.StaleNonce && this.Response == Digest.FormatResponse(method, this.Uri, this.UserName, password, realm, this.Nonce, this.NonceCount, this.CheckNonce, this.QualityOfProtection);
        }
    }
}